﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EnvDTE;
using System.IO;
using Microsoft.VisualStudio.CodeCoverage;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Windows.Forms;

namespace Osiris.Tools.CheckinPolicies
{
    /// <summary>
    /// Checkin Policy for enforcing that the current solution (at checkin time) has code coverage enabled and that the latest test run
    /// has a total code coverage above codeCoveragePercentLimit (configurable)
    /// </summary>
    [Serializable]
    public class CodeCoveragePolicy : OsirisPolicyBase
    {
        private int codeCoveragePercentLimit = 0;
        private const string NoUnitTest = "No unit tests have been executed for this solution";
        private const string TestResultsFolder = "TestResults";
        private const string BinariesFolder = "Out";
        private const string NoCoverageFile = "No coverage file has been generated";
        private const string MultipleCoverageFiles = "Multiple coverage files not supported";

        public override bool Edit(IPolicyEditArgs policyEditArgs)
        {
            using (ConfigureCodeCoveragePolicy dialog = new ConfigureCodeCoveragePolicy())
            {
                dialog.CodeCoveragePercentLimit = this.codeCoveragePercentLimit;
                if (dialog.ShowDialog(policyEditArgs.Parent) == DialogResult.OK)
                {
                    this.codeCoveragePercentLimit = dialog.CodeCoveragePercentLimit;
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        public override Microsoft.TeamFoundation.VersionControl.Client.PolicyFailure[] Evaluate()
        {
            List<PolicyFailure> failures = new List<PolicyFailure>(); ;

            try
            {
                _DTE dte = this.PendingCheckin.GetService(typeof(_DTE)) as _DTE;
                if (dte.Solution != null && !String.IsNullOrEmpty(dte.Solution.FileName))
                {
                    string solutionPath = Path.GetDirectoryName(dte.Solution.FullName);
                    string testResultsFolder = CodeCoveragePolicy.GetTestResultsFolder(solutionPath);
                    if (testResultsFolder == null)
                    {
                        failures.Add(new PolicyFailure(NoUnitTest, this));
                    }
                    string binariesFolder = CodeCoveragePolicy.GetTestRunBinariesFolder(testResultsFolder);
                    string codeCoverageFile = CodeCoveragePolicy.GetCodeCoverageFile(testResultsFolder);

                    decimal totalCodeCoveragePercent = CalculateTotalCodeCoverage(binariesFolder, codeCoverageFile);
                    if (totalCodeCoveragePercent < codeCoveragePercentLimit)
                    {
                        failures.Add(new PolicyFailure(Description, this));
                    }                        
                }
            }
            catch (Exception ex)
            {
                failures.Add(new PolicyFailure(ex.Message, this));
            }
            finally
            {
                // Seems to be needed in order to flush the code coverage information. If we don't do this, the BuildDataSet method call 
                // will return the same information agan,even if we do a new test run inbetween
                CoverageInfoManager.Shutdown();
            }
            return failures.ToArray();
        }

        private static decimal CalculateTotalCodeCoverage(string binariesFolder, string codeCoverageFile)
        {
            // Create a coverage info object from the file
            CoverageInfoManager.ExePath = binariesFolder;
            CoverageInfoManager.SymPath = binariesFolder;

            CoverageInfo ci = CoverageInfoManager.CreateInfoFromFile(codeCoverageFile);
            CoverageDS data = ci.BuildDataSet(null);

            uint blocksCovered = 0;
            uint blocksNotCovered = 0;
            foreach (CoverageDS.ModuleRow m in data.Module)
            {
                blocksCovered += m.BlocksCovered;
                blocksNotCovered += m.BlocksNotCovered;
            }

            return GetPercentCoverage(blocksCovered, blocksNotCovered);
        }

        /// <summary>
        /// Retreives the latest test result folder, which is located beneath the TestResults folder beneath the solution file.
        /// </summary>
        /// <param name="solutionPath">Path to the solution file</param>
        /// <returns></returns>
        public static string GetTestResultsFolder(string solutionPath)
        {
            string testResultsFolder = Path.Combine(solutionPath, TestResultsFolder);
            if (Directory.Exists(testResultsFolder))
            {
                DirectoryInfo info = new DirectoryInfo(testResultsFolder);
                DirectoryInfo[] dirs = info.GetDirectories();
                if( dirs.Length == 0 )
                    return null;

                if (dirs.Length > 1)
                {
                    // Sort by last write time
                    Array.Sort<DirectoryInfo>(dirs, delegate(DirectoryInfo x, DirectoryInfo y) { return y.LastWriteTime.CompareTo(x.LastWriteTime); });
                }

                return dirs[0].FullName;
            }
            return null;
        }

        /// <summary>
        /// Locates the data.coverage file. 
        /// </summary>
        /// <param name="testResultsFolder"></param>
        /// <returns></returns>
        private static string GetCodeCoverageFile(string testResultsFolder)
        {
            DirectoryInfo info = new DirectoryInfo(testResultsFolder);
            FileInfo[] coverageFiles = info.GetFiles("*.coverage", SearchOption.AllDirectories);
            if (coverageFiles.Length == 0)
                throw new Exception(NoCoverageFile);
            if (coverageFiles.Length > 1)
                throw new Exception(MultipleCoverageFiles);

            return coverageFiles[0].FullName;
        }

        /// <summary>
        /// Returns the path to the Binaries folder
        /// </summary>
        /// <param name="testResultsFolder"></param>
        /// <returns></returns>
        private static string GetTestRunBinariesFolder(string testResultsFolder)
        {
            return Path.Combine(testResultsFolder, BinariesFolder);
        }

        /// <summary>
        /// Calulates the coverage in percent from the blocks covered/not covered information
        /// </summary>
        /// <param name="dblCovered">Nr covered blocks</param>
        /// <param name="dblNot">Nr of not covered blocks</param>
        /// <returns>Percentage (decimal) total code coverage</returns>
        private static decimal GetPercentCoverage(uint covered, uint notCovered)
        {
            uint total = covered + notCovered;
            return Convert.ToDecimal(100 * (decimal)covered / (decimal)total);
        }


        public override string Description
        {
            get { return "Code coverage must be above " + codeCoveragePercentLimit + " percent"; }
        }

        public override string Type
        {
            get { return "Osiris: Code Coverage"; }
        }

        public override string TypeDescription
        {
            get { return "This policy will not allow people to check in code that doesn't exceed a certain code coverage limit."; }
        }

        public override bool CanEdit
        {
            get
            {
                return true;
            }
        }


    }
}
